/**
 * @file serial.h
 * @brief serial 
 * @author JakeYang <add358@gmail.com>
 * @version 0.1
 * @date 2016-03-10
 */
#include "serial.h"

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <termios.h>

struct _Serial {
	int fd;
    int parity;
    struct termios currentTermios;
    struct termios restoredTermios;
};

static inline int serial_update_termios(Serial *s)
{
    return tcsetattr(s->fd, TCSANOW, &s->currentTermios);
}

Serial *serial_new()
{
    Serial *s = (Serial *)malloc(sizeof(Serial));
    memset(s, 0, sizeof(Serial));
    return s;
}

Serial *serial_open(const char *name)
{
	if (!name) {
		return NULL;
	}

	int fd = open(name, O_RDWR | O_NOCTTY | O_NDELAY);
	if (fd < 0) {
		return NULL;
	}
    fcntl(fd, F_SETFL, 0);

    Serial *s = serial_new();
    s->fd = fd;
    tcgetattr(s->fd, &s->restoredTermios);
    memcpy(&s->currentTermios, &s->restoredTermios, sizeof(struct termios));

    serial_set_speed(s, BAUD_115200);
    serial_set_flow_control(s, NO_FLOW_CONTROL);
    serial_set_parity(s, NONE_PARITY);
    serial_set_databits(s, DATA_8);
    serial_set_stopbits(s, ONE_STOP);

	return s;
}

inline void serial_close(Serial *s)
{
	if (NULL == s) {
		return;
	}
    tcsetattr(s->fd, TCSANOW, &s->restoredTermios);
	close(s->fd);
	free(s);
}

int serial_set_speed(Serial *s, BaudRate baudRate)
{
    cfsetispeed(&s->currentTermios, baudRate);
    cfsetospeed(&s->currentTermios, baudRate);
    s->currentTermios.c_cflag |= (CLOCAL|CREAD);

    return serial_update_termios(s);
}

int serial_set_flow_control(Serial *s, FlowControl flowControl)
{   
    switch (flowControl) {
    case NO_FLOW_CONTROL:
        s->currentTermios.c_cflag &= ~CRTSCTS;
        s->currentTermios.c_iflag &= ~(IXON | IXOFF | IXANY);
        break;
    case HARDWARD_CONTROL:
        s->currentTermios.c_cflag |= CRTSCTS;
        s->currentTermios.c_iflag &= ~(IXON | IXOFF | IXANY);
        break;
    case SOFTWARD_CONTROL:
        s->currentTermios.c_cflag &= ~CRTSCTS;
        s->currentTermios.c_iflag |= IXON | IXOFF | IXANY;
        break;
    }

    return serial_update_termios(s);
}

int serial_set_parity(Serial *s, Parity parity)
{
    s->currentTermios.c_iflag &= ~(PARMRK | INPCK);
    s->currentTermios.c_iflag |= IGNPAR;

    switch (parity) {

#ifdef CMSPAR
    // Here Installation parity only for GNU/Linux where the macro CMSPAR.
    case SPACE_PARITY:
        s->currentTermios.c_cflag &= ~PARODD;
        s->currentTermios.c_cflag |= PARENB | CMSPAR;
        break;
    case MARK_PARITY:
        s->currentTermios.c_cflag |= PARENB | CMSPAR | PARODD;
        break;
#endif
    case NONE_PARITY:
        s->currentTermios.c_cflag &= ~PARENB;
        break;
    case EVENT_PARITY:
        s->currentTermios.c_cflag &= ~PARODD;
        s->currentTermios.c_cflag |= PARENB;
        break;
    case ODD_PARITY:
        s->currentTermios.c_cflag |= PARENB | PARODD;
        break;
    }

    s->parity = parity;

    return serial_update_termios(s);
}

int serial_set_databits(Serial *s, DataBits bits)
{
    s->currentTermios.c_cflag &= ~CSIZE;
    s->currentTermios.c_cflag |= bits;

    return serial_update_termios(s);
}

int serial_set_stopbits(Serial *s, StopBits bits)
{
    if (ONE_STOP == bits) {
        s->currentTermios.c_cflag &= ~CSTOPB;
    } else {
        s->currentTermios.c_cflag |= CSTOPB;
    }

    return serial_update_termios(s);
}

int serial_set_data_error_policy(Serial *s, DATA_ERROR_POLICY policy)
{
    tcflag_t parmrkMask = PARMRK;
#ifndef CMSPAR
    // in space/mark parity emulation also used PARMRK flag
    if (s->parity == QSerialPort::SpaceParity
            || s->parity == QSerialPort::MarkParity) {
        parmrkMask = 0;
    }
#endif //CMSPAR
    switch (policy) {
    case SKIP_POLICY:
        s->currentTermios.c_iflag &= ~parmrkMask;
        s->currentTermios.c_iflag |= IGNPAR | INPCK;
        break;
    case PASS_ZERO_POLICY:
        s->currentTermios.c_iflag &= ~(IGNPAR | parmrkMask);
        s->currentTermios.c_iflag |= INPCK;
        break;
    case IGNORE_POLICY:
        s->currentTermios.c_iflag &= ~INPCK;
        break;
    case STOP_RECEIVING_POLICY:
        s->currentTermios.c_iflag &= ~IGNPAR;
        s->currentTermios.c_iflag |= parmrkMask | INPCK;
        break;
    }
    return serial_update_termios(s);
}


inline int serial_read(Serial *s, char *buf, int len)
{
    if (!s) {
        return -1;
    }
    return read(s->fd, buf, len);
}

int serial_write(Serial *s, const char *buf, int len)
{
    if (!s) {
        return -1;
    }
    return write(s->fd, buf, len);
}
